package net.villonanny.entity;

import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.ConcurrentModificationException;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TimeZone;
import java.util.regex.Pattern;

import net.villonanny.ConfigManager;
import net.villonanny.Console;
import net.villonanny.ConversationException;
import net.villonanny.EventLog;
import net.villonanny.FatalException;
import net.villonanny.TimeWhenRunnable;
import net.villonanny.Util;
import net.villonanny.entity.Village.VillageRunInfo;
import net.villonanny.strategy.Strategy;
import net.villonanny.type.TribeType;

import org.apache.commons.configuration.SubnodeConfiguration;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.log4j.Logger;
import org.apache.log4j.NDC;
import org.slf4j.helpers.MessageFormatter;

public class Server extends Thread {
	private final static Logger log = Logger.getLogger(Server.class);
	private Util util;
	private SubnodeConfiguration config;
//	private Map<String, Village> villages;
	private List<Village> villages;
	
	private String loginUrl; // The login URL uniquely identifies a server
	private String user;
	private boolean enabled=false;
	private boolean keepRunning=true;
	private TribeType tribeType = null;
	
	private String timeZone;
	private int msShift; 

	public Server(SubnodeConfiguration serverConfig) {
		// Put here only values that don't need config refresh
		this.enabled = serverConfig.getBoolean("/@enabled");
		this.loginUrl = serverConfig.getString("/loginUrl");
		this.user = serverConfig.getString("/user");
		this.timeZone = serverConfig.getString("/timeZone");
		this.msShift = serverConfig.getInt("/msShift");
//		this.villages = new HashMap<String, Village>();
		this.villages = new ArrayList<Village>();
		// All other attributes must be fetched in updateConfig
		updateConfig(serverConfig);
	}
	
	private void setTribeType(SubnodeConfiguration serverConfig) {
		String tribeString = serverConfig.getString("@tribe");
		if (tribeString==null) {
			String desc = serverConfig.getString("@desc", "");
			String msg = Util.getLocalMessage("msg.noTribe", this.getClass());
			String message = MessageFormatter.format(msg, desc);
			throw new FatalException(message);
		}
		String tribeKey = util.getTranslator().getKeyword(tribeString);
		this.tribeType = TribeType.fromString(tribeKey); 
	}
	
	public Collection<Village> getVillages() {
//		return villages.values();
		return villages;
	}
	
	public String getServerDesc() {
		// When "desc" is not set, use the server url instead
		return config.getString("/@desc", getServerId());
	}
	
	public void run() {
		EventLog.log("Starting nanny for \"" + getServerDesc() + "\"");
		NDC.push(getServerDesc());
		try {
			// Nanny villages
//			while (keepRunning) {
				ConfigManager.reloadIfChanged();
				Console.getInstance().checkPause();
				TimeWhenRunnable timeWhenRunnable = null;
//				int constructionQueues = config.getInt("/@constructionQueues", 1);
				// Loop all enabled villages
				
				List<VillageRunner> villageRunners = new ArrayList<VillageRunner>();
				try {
					
					EventLog.log("Total villages to run: " + this.villages.size());
					
					TimeZone tz = TimeZone.getDefault();
					int localOffset = tz.getRawOffset()/(1000 * 60 * 60);
					EventLog.log("Local time zone: GMT" + (localOffset>0 ? "+" : "") + localOffset);
					EventLog.log("Server time zone: GMT" + timeZone);
					
					String tzToParse = timeZone;
					if (tzToParse.startsWith("+")) {
						tzToParse = tzToParse.substring(1);
					}
					int serverOffset = Integer.parseInt(tzToParse);
					
//					List<SubnodeConfiguration> villageConfigs = config.configurationsAt("/village[@enabled='true']");
					
//					for (SubnodeConfiguration villageConfig : villageConfigs) {
					for (Village village : this.villages) {
//						String url = villageConfig.getString("/url");
						String timeToRun = village.timeToRun;
//						String timeToRun = villageConfig.getString("/time");
//						Village village = villages.get(url);
//						if (village==null) {
//							log.warn("Configuration file not aligned to village map; ignoring village " + villageConfig.getString("/@desc") + " " + villageConfig.getString("/url"));
//							continue;
//						}
						
						village.server = this;
						EventLog.log("Updating village " + village.getDesc());
						
						// 1) calculate local time
						Calendar localTime = Calendar.getInstance();
						
						// 2) get main page with time data
						String mainPageData = village.gotoMainPage();
						
						village.update(); // update village from online page
//						EventLog.log("Calculating time...");
//						int vilageSecondsToGo = village.secondsToGo();
//						EventLog.log("Time to go from " + village.getDesc()+": "+ 
//								milliToTimeString(vilageSecondsToGo*1000));
						
						try {
							
							EventLog.log("Preparing village " + village.getDesc());
							
							VillageRunInfo villageRunInfo = village.execute();
							
							String[] datetime = timeToRun.split(" ");
							String[] dateItems = datetime[0].split("-");
							String[] timeItems = datetime[1].split(":");
							
							Calendar arriveTime = Calendar.getInstance();
							// run 4h
							arriveTime.set(Calendar.YEAR, Integer.valueOf(dateItems[0]));
							arriveTime.set(Calendar.MONTH, Integer.valueOf(dateItems[1])-1);
							arriveTime.set(Calendar.DAY_OF_MONTH, Integer.valueOf(dateItems[2]));
							arriveTime.set(Calendar.HOUR_OF_DAY, Integer.valueOf(timeItems[0]));
							arriveTime.set(Calendar.MINUTE, Integer.valueOf(timeItems[1]));
							arriveTime.set(Calendar.SECOND, Integer.valueOf(timeItems[2]));
							arriveTime.set(Calendar.MILLISECOND, 0);
							
							
							
							Calendar runTime = (Calendar) arriveTime.clone();
							runTime.add(Calendar.SECOND, -villageRunInfo.getSecondsToGo());
							
							EventLog.log("Arrival time (server): " + arriveTime.getTime());
							EventLog.log("Run time (server): " + runTime.getTime());
							
							// TODO: add time when to run village!!!
							String timeStartPattern = "<span id=\"tp1\" class=\"b\">";
							int idx = mainPageData.indexOf(timeStartPattern);
							String time = mainPageData.substring(idx + timeStartPattern.length(), 
									mainPageData.indexOf("<", idx + timeStartPattern.length()));
//							System.out.println("Run time is " + runTime.getTime());
//							System.out.println("Server time is " + time);
							
							String[] tmItems = time.split(":");
							
							Calendar serverTime = (Calendar) localTime.clone();
							serverTime.set(Calendar.HOUR_OF_DAY, Integer.valueOf(tmItems[0]));
							serverTime.set(Calendar.MINUTE, Integer.valueOf(tmItems[1]));
							serverTime.set(Calendar.SECOND, Integer.valueOf(tmItems[2]));
							serverTime.set(Calendar.MILLISECOND, 0);
							
							if (serverTime.get(Calendar.HOUR_OF_DAY) < localTime.get(Calendar.HOUR_OF_DAY)
									&& serverOffset > localOffset) {
								
								serverTime.add(Calendar.DAY_OF_MONTH, 1);
								
							}
							
							if (serverTime.get(Calendar.HOUR_OF_DAY) < localTime.get(Calendar.HOUR_OF_DAY)
									&& serverOffset < localOffset) {
								
								serverTime.add(Calendar.DAY_OF_MONTH, -1);
								
							}
							
//							System.out.println("Current time (server): " + serverTime.getTime());
//							System.out.println("Current time (local): " + localTime.getTime());
							
//							System.out.println("Calendered server time is " + serverTime.getTime());
							
							long secDiff =  (runTime.getTimeInMillis() - serverTime.getTimeInMillis()) / 1000;
							Calendar localRunTime = (Calendar) localTime.clone();
							
//							System.out.println("secDiff = " +secDiff);
							
							localRunTime.add(Calendar.SECOND, (int)secDiff);
							localRunTime.add(Calendar.MILLISECOND, msShift);
							
							village.runtime = localRunTime;
							
							
//							EventLog.log("Local run time shifted for " + msShift + " ms");
							EventLog.log("Run time (local): " + localRunTime.getTime());
							
//							Calendar sleepingTime = (Calendar) localRunTime.clone();
//							sleepingTime.add(Calendar.MINUTE, -3); // start strategies beforehand (3min)
							
							
							villageRunners.add( new VillageRunner(village, localRunTime, villageRunInfo) );

							
//							long msDiff =  sleepingTime.getTimeInMillis() - localTime.getTimeInMillis();
//							
//							if (msDiff > 0) {
//								System.out.println("Sleeping till " + sleepingTime.getTime());
//								System.out.println("Sleeping " + (msDiff/1000.0) + " sec");
//								Thread.sleep(msDiff);
//								System.out.println("Sleep ended...");
//							}
//							
//							if (village.getTimeWhenRunnable().before(new Date())) {
//								EventLog.log("Processing village \"" + village.getDesc() +"\"");
//								village.execute(); // Execute strategies
//							} else {
//								EventLog.log("Village \"" + village.getDesc() +"\" still sleeping");
//							}
//							TimeWhenRunnable newTimeWhenRunnable = village.getTimeWhenRunnable();
//							if (timeWhenRunnable==null || timeWhenRunnable.after(newTimeWhenRunnable)) {
//								timeWhenRunnable = newTimeWhenRunnable;
//							}
						} catch (SkipVillageRequested e) {
							log.debug("Village skipped");
							continue;
						} catch (SkipRequested e) {
							// Just keep going
							log.debug("Action skipped");
						} catch (ServerFatalException e) {
							throw e; // Will catch below
						} catch (Exception e) {
							String s = "Village \"" + village.getDesc() + "\" error (retrying later): ";
							log.error(s, e);
							EventLog.log(s + e.getMessage());
//							// Reset village
//							this.villages.remove(village.getVillageUrlString());
//							updateConfig(this.config);
//							//
							Util.shortestPause(false); // Just to be safe
							timeWhenRunnable = null; // Retry after userPause
//							break; // Must exit the loop to avoid ConcurrentModificationException after the updateConfig
						}
					}
				} catch (ConcurrentModificationException e) {
					// This happens when adding/removing village from configuration while server is running
					// Just ignore
					log.debug("Village list was modified while server running (ConcurrentModificationException): skipping and repeating");
				}
				
				if (villageRunners.isEmpty()) {
					return;
				}
				
//				log.info("Villages done");
				EventLog.log("Villages will be executed in the following order: ");
				Collections.sort( villageRunners );
				
				int idx = 1;
				for (VillageRunner runner : villageRunners) {
					EventLog.log(idx + ": " + runner.village.getDesc()+" will run at " + runner.runtime.getTime());
					idx++;
				}
				
				if (villageRunners.get(0).runtime.before(Calendar.getInstance())) {
					EventLog.log("First village runtime (" + villageRunners.get(0).runtime.getTime() + ") is in the past.");
					return;
				}
				
				// calculate intervals
				if (villageRunners.size() > 1) {
					long[] intervals = new long[villageRunners.size()];
					long minInterval = Long.MAX_VALUE;
					int minIndex = -1;
					for (int j=0; j<villageRunners.size()-1; j++) {
						intervals[j] = villageRunners.get(j+1).runtime.getTimeInMillis() -
							villageRunners.get(j).runtime.getTimeInMillis();
						if (intervals[j] < minInterval) {
							minInterval = intervals[j];
							minIndex = j;
						}
					}
					
					EventLog.log("Min. interval between attacks " + (minIndex+1)+"-"+(minIndex+2) + ": " + milliToTimeString(minInterval));
					if (minInterval < 10*1000) {
						EventLog.log("Min. interval between attacks is less than 10 sec");
						return;
					}
				}
				
				
				
//				for (VillageRunner runner : villageRunners) {
//					EventLog.log("Preparing waves for " + runner.village.getDesc());
//					runner.prepare();
//				}
				
				// wait for update and run 
				for (VillageRunner runner : villageRunners) {
					long msToWait = runner.updateTime.getTimeInMillis() - Calendar.getInstance().getTimeInMillis();
					if (msToWait > 0) {
						EventLog.log("Sleeping " + milliToTimeString(msToWait)+" before running " + runner.village.getDesc());
						Thread.sleep(msToWait);
					}
					EventLog.log("Updating " + runner.village.getDesc());
					runner.update();
					EventLog.log("Waiting for " + runner.village.getDesc() +" to run");
					while (runner.runtime.after(Calendar.getInstance())) {
						Thread.sleep(10);
					}
					EventLog.log("Running " + runner.village.getDesc() +"...");
					runner.run();
					EventLog.log("Village " + runner.village.getDesc() +" done");
				}
				
//				boolean sharp = false;
//				long milliPause = 0;
//				if (timeWhenRunnable!=null) {
//					sharp = timeWhenRunnable.isSharp();
//					milliPause = timeWhenRunnable.getTime() - System.currentTimeMillis();
//					if (milliPause<0) {
//						milliPause=0;
//					}
//					int pauseLimit = config.getInt("/@pauseLimit", 86400) * 1000; //  Max 24h
//					if (milliPause > pauseLimit) {
//						log.debug("Limiting pause from " + milliPause + " to " + pauseLimit);
//						milliPause = pauseLimit;
//					}
//					log.debug("Next available action in " + util.milliToTimeString(milliPause));
//				}
//				if (keepRunning) {
//					util.userPause(milliPause, sharp);
//					Util.shortestPause(sharp); // Just to be safe
//				}
//			}
		} catch (Exception e) {
			EventLog.log(e.getMessage());
			log.error("", e);
			setEnabledAndStartStop(false);
		} finally {
			NDC.remove(); // not pop()
		}
		
		System.out.println("Server thread finished.");
	}

	public void login() throws ConversationException {
		util.login(false);
	}

	public boolean isEnabled() {
		return enabled;
	}

	public void setEnabledAndStartStop(boolean enabled) {
		boolean previousState = this.enabled;
		this.enabled = enabled;
		if (enabled==false && previousState==true) {
			terminate();
		} else if (enabled==true && previousState==false) {
			begin();
		}
	}
	
	public void begin() {
		if (enabled) {
			EventLog.log("evt.serverEnabled", this.getClass(), this.getServerDesc());
			keepRunning = true;
			start();
			Console.getInstance().addServerThread(this);
		}
	}
	
	public void terminate() {
		EventLog.log("evt.serverStopped", this.getClass(), this.getServerDesc());
		keepRunning = false;
		interrupt();
		Console.getInstance().removeServerThread(this);
	}

	public String getLoginUrl() {
		return loginUrl;
	}
	
	public static String idFromConfig(SubnodeConfiguration serverConfig) {
		String loginUrl = serverConfig.getString("/loginUrl");
		String user = serverConfig.getString("/user");
		return user + "@" + loginUrl;
	}
	
	public String getServerId() {
		return user + "@" + loginUrl;
	}

	public void updateConfig(SubnodeConfiguration serverConfig) {
		log.debug("Updating server config for " + getServerId());
		this.config = serverConfig;
		this.util = new Util(serverConfig);
		setTribeType(serverConfig);
		// Update villages
		// When a village is disabled or removed from the configuration, it is deleted from this.villages
//		List<Village> deletableVillages = new ArrayList<Village>(villages.values());
		// Loop all enabled villages
		List<SubnodeConfiguration> villageConfigs = serverConfig.configurationsAt("/village[@enabled='true']");
		for (SubnodeConfiguration villageConfig : villageConfigs) {
			// url is used as key
			String url = villageConfig.getString("/url");
//			Village village = this.villages.get(url);
			Village village = null;
			if (village==null) {
				// New village
				int constructionQueues = serverConfig.getInt("/@constructionQueues", 1);
				village = new Village(util, villageConfig, constructionQueues);
				village.timeToRun = villageConfig.getString("/time");
//				this.villages.put(url, village);
				this.villages.add(village);
			}/* else {
				// Village already exists
				village.updateConfig(villageConfig, util);
				deletableVillages.remove(village); // This village is still enabled			
			}*/
		}
		// Removing deleted or disabled villages
//		for (Village village : deletableVillages) {
//			this.villages.remove(village.getVillageUrlString());
//			village.terminate();
//		}
	}

	public TribeType getTribeType() {
		return tribeType;
	}
	
	public String milliToTimeString(long milliPause) {
		long hours = milliPause / 3600000;
		long min = (milliPause - hours*3600000) / 60000;
		long sec = (milliPause % 60000) / 1000;
		return String.format("%02d:%02d:%02d", hours , min , sec); // TODO aggiungere lo zero iniziale se una cifra
	}
	
	class VillageRunner implements /*Runnable,*/ Comparable<VillageRunner> {

		private Village village;
		private Calendar updateTime;
		private Calendar runtime;
		private List<PostMethod> methods;
		
		VillageRunner(Village village, Calendar runtime, VillageRunInfo villageRunInfo) {
			this.village = village;
			this.runtime = runtime;
			this.updateTime = (Calendar) runtime.clone();
			this.updateTime.add(Calendar.SECOND, -5);
			this.methods = villageRunInfo.getMethods();
		}
		
//		public void prepare() throws ConversationException {
//			methods = this.village.execute().getMethods();
//		}
		
		public void update() throws ConversationException {
			this.village.gotoMainPage(true);
		}

        class RunThread extends Thread{
            PostMethod postMethod;

            RunThread(PostMethod postMethod) {
                this.postMethod = postMethod;
            }

            @Override
            public void run() {
                try{
                    util.httpQuickPostPage(postMethod);
                }catch (ConversationException e){
                    e.printStackTrace();
                }
            }
        }

		public void run() throws ConversationException {
			
			for (PostMethod postMethod : this.methods) {
                new RunThread(postMethod).start();
                try{
                    Thread.sleep(40);
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
                EventLog.log("run wave");
			}
			
		}

		public int compareTo(VillageRunner o) {
			if (this.runtime.before(o.runtime)) {
				return -1;
			} else if (this.runtime.after(o.runtime)) {
				return 1;
			} else {
				return 0;
			}
		}
		
		@Override
		public String toString() {
			return "VilageRunner(" + this.village.getDesc() + ","+ runtime.getTime() +")";
		}
		
	}
}
